package masterserver;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.Semaphore;

import shared.*;

public class MasterServer extends java.rmi.server.UnicastRemoteObject implements
	MasterServerInterface
{
    // fileID to FileDescriptor
    private HashMap<Long, FileDescriptor> files;

    private long fileCount = 0;

    private Semaphore filesSemaphore = new Semaphore(1);

    private Semaphore fileCountSemaphore = new Semaphore(1);

    // username to User
    private HashMap<String, User> users;

    private Semaphore usersSemaphore = new Semaphore(1);

    // DataServerLocation to DataServerState
    private HashMap<String, DataServerState> dataServers;

    private Semaphore dataServersSemaphore = new Semaphore(1);

    private String myHostname;

    private LogManager logManager;

    private boolean isLogChanged = false;

    public MasterServer(String newHostname) throws RemoteException
    {
	super(Constants.PORT);

	Thread housekeepThread = new Thread(new Housekeeper());
	housekeepThread.start();
	Thread backUpLogThread = new Thread(new BackUpLogThread());
	backUpLogThread.start();

	this.myHostname = newHostname;

	this.users = new HashMap<String, User>();

	this.files = new HashMap<Long, FileDescriptor>();

	this.dataServers = new HashMap<String, DataServerState>();

	this.logManager = new LogManager(0);

	this.restoreFromDisk();

    }

    @SuppressWarnings("unchecked")
    public void restoreFromDisk()
    {

	String[] names = new String[] { "files", "users", "dataservers", "logs" };
	long[] restoredFrom = new long[] { 0, 0, 0 };

	for (int i = 0; i < names.length; i++)
	{
	    String folderName = Constants.MASTERSERVER_METAFILES_DIR + names[i]
		    + "/";
	    File folder = new File(folderName);
	    File[] files = folder.listFiles();
	    if (files.length < 2)
	    {
		System.out.println("No previous " + names[i]
			+ " backup on disk.");
		continue;
	    }

	    ArrayList<Long> fileNums = new ArrayList<Long>();
	    for (File file : files)
	    {
		try
		{
		    fileNums.add(Long.parseLong(file.getName()));
		} catch (Exception e)
		{
		    continue;
		}
	    }

	    // sort in ascending order..
	    java.util.Collections.sort(fileNums);
	    // fileNum.size() - 2 guarantees integrity of the file
	    Long biggest, realBiggest;
	    if (fileNums.size() >= 2)
	    {
		biggest = fileNums.get(fileNums.size() - 2);
		realBiggest = fileNums.get(fileNums.size() - 1);
	    } else
	    {
		biggest = fileNums.get(fileNums.size() - 1);
		realBiggest = biggest;
	    }

	    Path file = Paths.get(folderName + biggest.toString());
	    Path realBiggestFile = Paths.get(folderName
		    + realBiggest.toString());

	    BufferedInputStream inputStream = null;
	    ObjectInputStream in = null;

	    try
	    {
		inputStream = new BufferedInputStream(new FileInputStream(
			file.toString()));
		in = new ObjectInputStream(inputStream);

		// importing "files"
		if (i == 0)
		{
		    this.filesSemaphore.acquire();
		    this.files = (HashMap<Long, FileDescriptor>) in
			    .readObject();
		    System.out
			    .println("Successfully restored from files backup.");
		    restoredFrom[i] = biggest;
		}
		// importing "users"
		else if (i == 1)
		{
		    this.usersSemaphore.acquire();
		    this.users = (HashMap<String, User>) in.readObject();
		    System.out
			    .println("Successfully restored from users backup.");
		    restoredFrom[i] = biggest;
		}
		// importing "dataservers"
		else if (i == 2)
		{
		    this.dataServersSemaphore.acquire();
		    this.dataServers = (HashMap<String, DataServerState>) in
			    .readObject();
		    System.out
			    .println("Successfully restored from dataservers backup.");
		    restoredFrom[i] = biggest;
		}
		// restore the logs
		else if (i == 3)
		{
		    if (biggest != 0 && biggest == restoredFrom[0]
			    && biggest == restoredFrom[1]
			    && biggest == restoredFrom[2])
		    {
			System.out
				.println("Now master server will try to restore from logs.");
			if (true)
			{
			    try
			    {
				System.out
					.println("Trying to restore from log file "
						+ biggest);
				LogEntry entry = (LogEntry) in.readObject();

				while (entry != null)
				{
				    try
				    {
					Method method = this.getClass()
						.getMethod(entry.methodName,
							entry.types);
					method.invoke(this, entry.args);
					System.out
						.println("Successfully re-did the method: "
							+ entry.methodName);
				    } catch (Exception methodException)
				    {
					methodException.printStackTrace();
				    }
				    entry = (LogEntry) in.readObject();
				}

			    } catch (Exception readException)
			    {
				System.out
					.println("Completed one set of log entries restore.");
				// readException.printStackTrace();
			    }

			    if (realBiggest > biggest)
			    {
				try
				{
				    inputStream = new BufferedInputStream(
					    new FileInputStream(
						    realBiggestFile.toString()));
				    in = new ObjectInputStream(inputStream);
				} catch (Exception closeException)
				{
				    // closeException.printStackTrace();
				}

				try
				{
				    System.out
					    .println("Trying to restore from log file "
						    + realBiggest);
				    LogEntry entry = (LogEntry) in.readObject();
				    while (entry != null)
				    {
					try
					{
					    Method method = this.getClass()
						    .getMethod(
							    entry.methodName,
							    entry.types);
					    method.invoke(this, entry.args);
					    System.out
						    .println("Successfully re-did the method: "
							    + entry.methodName);
					} catch (Exception methodException)
					{
					    methodException.printStackTrace();
					}
					entry = (LogEntry) in.readObject();
				    }

				} catch (Exception readException)
				{
				    System.out
					    .println("Completed all log entries restore.");
				    // readException.printStackTrace();
				}
			    }

			}
		    }

		}

	    } catch (Exception e)
	    {
		e.printStackTrace();
		System.out.println("Failed to readin files/users/dataservers: "
			+ i);
	    } finally
	    {
		if (i == 0)
		{
		    if (this.files == null)
			this.files = new HashMap<Long, FileDescriptor>();
		    this.fileCount = this.files.size();
		    this.filesSemaphore.release();
		} else if (i == 1)
		{
		    if (this.users == null)
			this.users = new HashMap<String, User>();
		    this.usersSemaphore.release();
		} else if (i == 2)
		{
		    if (this.dataServers == null)
			this.dataServers = new HashMap<String, DataServerState>();
		    this.dataServersSemaphore.release();
		} else if (i == 3)
		{
		    this.logManager.changeLogFile(realBiggest);
		}

		try
		{
		    in.close();
		    inputStream.close();

		} catch (Exception e)
		{
		}
	    }

	}

    }

    @Override
    public String hello() throws RemoteException
    {
	return "Master Server Connected!";
    }

    @Override
    public User signUp(User newUser) throws RemoteException
    {
	try
	{
	    Method thisMethod = this.getClass().getMethod("signUp",
		    new Class[] { User.class });
	    Object[] args = new Object[] { newUser };
	    LogEntry newEntry = new LogEntry();
	    newEntry.methodName = thisMethod.getName();
	    newEntry.args = args;
	    newEntry.types = thisMethod.getParameterTypes();
	    newEntry.isMethodCall = true;
	    this.logManager.addEntry(newEntry);
	    this.isLogChanged = true;
	} catch (NoSuchMethodException | SecurityException ee)
	{
	    ee.printStackTrace();
	}

	String username = newUser.getUsername();

	if (username == null || username.length() == 0)
	    throw new CloudException("Username cannot be empty!");

	if (users.get(username) != null)
	    throw new CloudException("Username already used!");

	if (newUser.getPassword() == null || newUser.getPassword().length() < 3)
	    throw new CloudException("Password is too short!");

	User user = new User();
	user.setUsername(username);
	user.setPassword(newUser.getPassword());
	user.setFirstname(newUser.getFirstname());
	user.setLastname(newUser.getLastname());
	user.setEmail(newUser.getEmail());
	user.setMyFileIDs(new ArrayList<Long>());
	user.setMyFiles(null);
	user.setMyInvites(new ArrayList<ShareInvite>());

	try
	{
	    this.usersSemaphore.acquire();
	    this.users.put(username, user);
	} catch (InterruptedException e)
	{
	    throw new CloudException(
		    "Failed to update master server user list!");
	} finally
	{
	    this.usersSemaphore.release();
	}

	return user;
    }

    @Override
    public User logOn(String username, String password) throws RemoteException
    {
	// username is always lower-cased
	username = username.toLowerCase();

	User theUser = this.users.get(username);

	// username not exists
	if (theUser == null)
	    throw new CloudException("Username/password match is not found!");

	// password does not match
	if (!theUser.getPassword().equals(password))
	    throw new CloudException("Username/password match is not found!");

	theUser.populate(this.files);

	return theUser;
    }

    @Override
    public User getUser(String username) throws RemoteException
    {
	User user = null;
	user = this.users.get(username);
	user.populate(this.files);
	return user;
    }

    @Override
    public void registerDataserver(DataServerState state)
	    throws RemoteException
    {

	System.out.println("Heartbeat received from dataserver@"
		+ state.getHostName());
	if (state.getHostName() != null && state.getHostName().length() != 0)
	{
	    try
	    {
		Calendar calendar = new java.util.GregorianCalendar();
		state.setLastActiveTime(calendar.getTimeInMillis());
		this.dataServersSemaphore.acquire();
		this.dataServers.put(state.getHostName(), state);
	    } catch (Exception e)
	    {
		e.printStackTrace();
	    } finally
	    {
		this.dataServersSemaphore.release();
	    }
	}
    }

    @Override
    public void completeWrite(DataServerState dataServerState,
	    FileDescriptor fileDescriptor) throws RemoteException
    {
	try
	{
	    Method thisMethod = this.getClass()
		    .getMethod(
			    "completeWrite",
			    new Class[] { DataServerState.class,
				    FileDescriptor.class });
	    Object[] args = new Object[] { dataServerState, fileDescriptor };
	    LogEntry newEntry = new LogEntry();
	    newEntry.methodName = thisMethod.getName();
	    newEntry.args = args;
	    newEntry.types = thisMethod.getParameterTypes();
	    newEntry.isMethodCall = true;
	    this.logManager.addEntry(newEntry);
	    this.isLogChanged = true;
	} catch (NoSuchMethodException | SecurityException ee)
	{
	    ee.printStackTrace();
	}

	if (this.files.get(fileDescriptor.getFileID()) == null)
	{
	    ArrayList<DataServerState> dataServers = new ArrayList<DataServerState>();
	    dataServers.add(dataServerState);
	    fileDescriptor.setDataServers(dataServers);
	    fileDescriptor.setLocked(false);
	    fileDescriptor.setVersion(1);
	    try
	    {
		this.filesSemaphore.acquire();
		this.files.put(fileDescriptor.getFileID(), fileDescriptor);
	    } catch (Exception e)
	    {
		e.printStackTrace();
	    } finally
	    {
		this.filesSemaphore.release();
	    }

	    // add file into the user's file list
	    try
	    {
		this.usersSemaphore.acquire();
		for (int i = 0; i < fileDescriptor.getOwners().size(); i++)
		{
		    if (!this.users.get(fileDescriptor.getOwners().get(i))
			    .getMyFileIDs()
			    .contains(fileDescriptor.getFileID()))
			this.users.get(fileDescriptor.getOwners().get(i))
				.getMyFileIDs().add(fileDescriptor.getFileID());
		}
	    } catch (Exception e)
	    {
		e.printStackTrace();
	    } finally
	    {
		this.usersSemaphore.release();
	    }

	} else
	{

	    FileDescriptor currentDescriptor = this.files.get(fileDescriptor
		    .getFileID());

	    // if file is updated:
	    if (!currentDescriptor.getHash().equals(fileDescriptor.getHash()))
	    {

		ArrayList<DataServerState> dataServers = new ArrayList<DataServerState>();
		dataServers.add(dataServerState);
		try
		{
		    this.filesSemaphore.acquire();
		    currentDescriptor.setDataServers(dataServers);
		    currentDescriptor.setHash(fileDescriptor.getHash());
		    currentDescriptor
			    .setVersion(currentDescriptor.getVersion() + 1);
		    currentDescriptor.setLocked(false);

		} catch (Exception e)
		{
		    e.printStackTrace();
		} finally
		{
		    this.filesSemaphore.release();
		}

	    }
	    // file not updated, simply increase replication number
	    else
	    {
		int i = 0;
		for (i = 0; i < currentDescriptor.getDataServers().size(); i++)
		    if (currentDescriptor.getDataServers().get(i).getHostName()
			    .equals(dataServerState.getHostName()))
			break;
		if (i == currentDescriptor.getDataServers().size())
		{
		    try
		    {
			this.filesSemaphore.acquire();
			currentDescriptor.getDataServers().add(dataServerState);
		    } catch (Exception e)
		    {
			e.printStackTrace();
		    } finally
		    {
			this.filesSemaphore.release();
		    }
		}
	    }
	}
    }

    @Override
    public FileDescriptor requestCreate(FileDescriptor newFile)
	    throws RemoteException
    {
	try
	{
	    Method thisMethod = this.getClass().getMethod("requestCreate",
		    new Class[] { FileDescriptor.class });
	    Object[] args = new Object[] { newFile };
	    LogEntry newEntry = new LogEntry();
	    newEntry.methodName = thisMethod.getName();
	    newEntry.args = args;
	    newEntry.types = thisMethod.getParameterTypes();
	    newEntry.isMethodCall = true;
	    this.logManager.addEntry(newEntry);
	    this.isLogChanged = true;
	} catch (NoSuchMethodException | SecurityException ee)
	{
	    ee.printStackTrace();
	}

	// test if the user exists
	String username = null;
	try
	{
	    username = newFile.getOwners().get(0);
	    if (username == null || this.users.get(username) == null)
		throw new CloudException(
			"Username does not exists. File creation failed.");
	} catch (Exception e)
	{
	    throw new CloudException(
		    "Username does not exists. File creation failed.");
	}

	long newFileID = this.fileCount;
	try
	{
	    this.fileCountSemaphore.acquire();
	    while (this.files.containsKey(this.fileCount))
		this.fileCount++;
	    newFileID = this.fileCount;
	    this.fileCount++;
	} catch (InterruptedException e)
	{
	    throw new CloudException("File creation failed, please try again.");
	} finally
	{
	    this.fileCountSemaphore.release();
	}

	newFile.setFileID(newFileID);

	// no need to trouble data server if it's a directory
	if (newFile.isDirectory())
	{
	    try
	    {
		newFile.setDeleted(false);
		newFile.setVersion(0);
		this.filesSemaphore.acquire();
		this.files.put(newFileID, newFile);
		this.usersSemaphore.acquire();
		if (!this.users.get(username).getMyFileIDs()
			.contains(newFileID))
		    this.users.get(username).getMyFileIDs().add(newFileID);
		return newFile;
	    } catch (Exception e)
	    {
		throw new CloudException(
			"File creation failed, please try again.");
	    } finally
	    {
		this.filesSemaphore.release();
		this.usersSemaphore.release();
	    }
	} else
	{

	    ArrayList<DataServerState> bestDataServers = new ArrayList<DataServerState>(
		    this.getBestDataServers());
	    newFile.setDataServers(bestDataServers);

	    int i = 0;
	    for (i = newFile.getDataServers().size() - 1; i >= 0; i--)
	    {
		try
		{
		    newFile.getDataServers().get(i).getRemoteDataServer()
			    .createFile(newFileID);
		} catch (RemoteException e)
		{
		    // e.printStackTrace();
		    newFile.getDataServers().remove(i);
		    continue;
		}
	    }

	    return newFile;
	}
    }

    @SuppressWarnings("finally")
    @Override
    public FileDescriptor requestModify(FileDescriptor theFile)
	    throws RemoteException
    {
	FileDescriptor currentDescriptor = this.files.get(theFile.getFileID());
	if (currentDescriptor == null || currentDescriptor.isDeleted()
		|| currentDescriptor.isDirectory())
	    throw new CloudException("The requested file does not exist!");
	if (currentDescriptor.isLocked())
	    throw new CloudException("The requested file "
		    + currentDescriptor.getPathname() + " is being locked!");
	if (currentDescriptor.getVersion() > theFile.getVersion())
	{
	    throw new CloudException("Version conflict!");
	} else
	{
	    try
	    {
		this.filesSemaphore.acquire();
		if (currentDescriptor.isLocked() == false)
		{
		    currentDescriptor.setLocked(true);
		} else
		    throw new CloudException(
			    "The requested file is being locked!");
	    } catch (CloudException e1)
	    {
		throw e1;
	    } catch (Exception e2)
	    {
		e2.printStackTrace();
	    } finally
	    {
		this.filesSemaphore.release();
	    }

	    // this gives u a COPY of dataservers and owners field
	    FileDescriptor returnedDescriptor = new FileDescriptor(
		    currentDescriptor);
	    if (returnedDescriptor.getDataServers().size() >= 3)
		return returnedDescriptor;
	    // this deals with handling lack of replications
	    else
	    {
		try
		{
		    ArrayList<DataServerState> bestDataServers = new ArrayList<DataServerState>(
			    this.getBestDataServers());
		    for (DataServerState newServer : bestDataServers)
		    {
			if (returnedDescriptor.getDataServers().size() >= 3)
			    break;
			boolean isSame = false;
			for (DataServerState currentServer : returnedDescriptor
				.getDataServers())
			    if (currentServer.getHostName().equals(
				    newServer.getHostName()))
				isSame = true;
			if (isSame == false)
			    returnedDescriptor.getDataServers().add(newServer);
		    }
		} catch (Exception e)
		{

		} finally
		{
		    return returnedDescriptor;
		}
	    }

	}
    }

    @Override
    public boolean requestDelete(long fileID) throws RemoteException
    {
	try
	{
	    Method thisMethod = this.getClass().getMethod("requestDelete",
		    new Class[] { long.class });
	    Object[] args = new Object[] { fileID };
	    LogEntry newEntry = new LogEntry();
	    newEntry.methodName = thisMethod.getName();
	    newEntry.args = args;
	    newEntry.types = thisMethod.getParameterTypes();
	    newEntry.isMethodCall = true;
	    this.logManager.addEntry(newEntry);
	    this.isLogChanged = true;
	} catch (NoSuchMethodException | SecurityException ee)
	{
	    ee.printStackTrace();
	}
	FileDescriptor fileDescriptor = null;
	try
	{
	    this.filesSemaphore.acquire();
	    this.usersSemaphore.acquire();
	    fileDescriptor = this.files.get(fileID);
	    fileDescriptor.setDeleted(true);
	    ArrayList<String> owners = fileDescriptor.getOwners();

	    for (int i = 0; i < owners.size(); i++)
	    {
		while (true)
		{
		    User user = this.users.get(owners.get(i));
		    if (user == null)
			break;
		    int index = user.getMyFileIDs().indexOf(fileID);
		    if (index < 0)
			break;
		    user.getMyFileIDs().remove(index);
		}
	    }

	    fileDescriptor.setOwners(new ArrayList<String>());

	} catch (Exception e)
	{
	    throw new CloudException("Deletion failed!");
	} finally
	{
	    this.filesSemaphore.release();
	    this.usersSemaphore.release();
	}

	return true;
    }

    @Override
    public FileDescriptor getLatestFileDescriptor(long fileID)
	    throws RemoteException
    {
	return this.files.get(fileID);
    }

    @SuppressWarnings("unchecked")
    public List<DataServerState> getBestDataServers()
    {
	Collection<DataServerState> collection = this.dataServers.values();
	ArrayList<DataServerState> list = new ArrayList<DataServerState>(
		collection);
	java.util.Collections.sort(list);
	List<DataServerState> newList = new ArrayList<DataServerState>();
	if (list.size() >= 3)
	    newList = list.subList(0, 3);
	else if (list.size() == 2)
	    newList = list.subList(0, 2);
	else if (list.size() == 1)
	    newList = list.subList(0, 1);

	return newList;
    }

    @Override
    public void requestShare(long fileID, String username)
	    throws RemoteException
    {
	try
	{
	    Method thisMethod = this.getClass().getMethod("requestShare",
		    new Class[] { long.class, String.class });
	    Object[] args = new Object[] { fileID, username };
	    LogEntry newEntry = new LogEntry();
	    newEntry.methodName = thisMethod.getName();
	    newEntry.args = args;
	    newEntry.types = thisMethod.getParameterTypes();
	    newEntry.isMethodCall = true;
	    this.logManager.addEntry(newEntry);
	    this.isLogChanged = true;
	} catch (NoSuchMethodException | SecurityException ee)
	{
	    ee.printStackTrace();
	}

	User user = this.users.get(username);
	if (user == null)
	    throw new CloudException("Username does not exist!");
	if (this.files.get(fileID) == null)
	    throw new CloudException("FileID does not exist!");
	if (this.files.get(fileID).isDeleted())
	    throw new CloudException("File was already deleted!");

	ShareInvite invite = new ShareInvite();
	invite.setFileID(fileID);
	try
	{
	    invite.setInitiator(this.files.get(fileID).getOwners().get(0));
	    invite.setFileName(this.files.get(fileID).getPathname());
	} catch (Exception e)
	{
	    e.printStackTrace();
	}

	try
	{
	    this.usersSemaphore.acquire();
	    boolean isSame = false;
	    for (ShareInvite inv : user.getMyInvites())
		if (inv.getFileID() == fileID)
		    isSame = true;
	    if (isSame == false)
		user.getMyInvites().add(invite);
	} catch (Exception e)
	{
	    e.printStackTrace();
	} finally
	{
	    this.usersSemaphore.release();
	}
    }

    @Override
    public void confirmShare(long fileID, String username)
	    throws RemoteException
    {
	try
	{
	    Method thisMethod = this.getClass().getMethod("confirmShare",
		    new Class[] { long.class, String.class });
	    Object[] args = new Object[] { fileID, username };
	    LogEntry newEntry = new LogEntry();
	    newEntry.methodName = thisMethod.getName();
	    newEntry.args = args;
	    newEntry.types = thisMethod.getParameterTypes();
	    newEntry.isMethodCall = true;
	    this.logManager.addEntry(newEntry);
	    this.isLogChanged = true;
	} catch (NoSuchMethodException | SecurityException ee)
	{
	    ee.printStackTrace();
	}

	try
	{
	    this.filesSemaphore.acquire();
	    this.usersSemaphore.acquire();
	    FileDescriptor ds = this.files.get(fileID);
	    if (ds == null || ds.isDeleted())
		throw new CloudException("FileID does not exist!");

	    User user = this.users.get(username);
	    if (user == null)
		throw new CloudException("Username does not exist!");

	    if (!ds.getOwners().contains(username))
		ds.getOwners().add(username);
	    if (!user.getMyFileIDs().contains(fileID))
		user.getMyFileIDs().add(fileID);
	    for (int i = user.getMyInvites().size() - 1; i >= 0; i--)
	    {
		ShareInvite inv = user.getMyInvites().get(i);
		if (inv.getFileID() == fileID)
		    user.getMyInvites().remove(i);
	    }
	} catch (Exception e)
	{
	    e.printStackTrace();
	    throw new CloudException("Failed to confirm share!");
	} finally
	{
	    this.filesSemaphore.release();
	    this.usersSemaphore.release();
	}

    }

    public void close()
    {

    }

    class Housekeeper implements Runnable
    {

	@Override
	public void run()
	{
	    try
	    {
		// initial sleep: wait for a while before starting housekeeping
		Thread.sleep(Constants.DATASERVER_EXPIRY_LENGTH * 2);
	    } catch (InterruptedException e)
	    {
		e.printStackTrace();
	    }

	    while (true)
	    {
		try
		{
		    Thread.sleep(Constants.DATASERVER_EXPIRY_LENGTH / 3);
		} catch (InterruptedException e)
		{
		    e.printStackTrace();
		}
		housekeepDataServers();
		backUpToDisk();
	    }
	}

	public void housekeepDataServers()
	{
	    try
	    {
		dataServersSemaphore.acquire();

		for (String hostName : dataServers.keySet())
		{
		    DataServerState state = dataServers.get(hostName);
		    Calendar calendar = new java.util.GregorianCalendar();
		    long now = calendar.getTimeInMillis();

		    if (now - state.getLastActiveTime() > Constants.DATASERVER_EXPIRY_LENGTH
			    || now < state.getLastActiveTime())
		    {
			String hostname = state.getHostName();

			filesSemaphore.acquire();
			usersSemaphore.acquire();
			ArrayList<Long> fileIDs = state.getFileIDs();
			for (Long fileID : fileIDs)
			{
			    if (files.get(fileID) == null)
				continue;
			    for (int i = files.get(fileID).getDataServers()
				    .size() - 1; i >= 0; i--)
			    {
				if (files.get(fileID).getDataServers().get(i)
					.getHostName().equals(hostname))
				{
				    files.get(fileID).getDataServers()
					    .remove(i);
				}
			    }
			}
			filesSemaphore.release();
			usersSemaphore.release();

			dataServers.remove(hostname);
			System.out.println("Dataserver " + hostname
				+ " is down and removed from server list!");
			break;
		    }
		}
	    } catch (Exception e)
	    {
		e.printStackTrace();
	    } finally
	    {
		dataServersSemaphore.release();
	    }

	}

	public void backUpToDisk()
	{

	    java.util.Date date = new java.util.Date();
	    long mills = date.getTime();
	    String[] names = new String[] { "files", "users", "dataservers",
		    "logs" };

	    for (int i = 0; i < names.length; i++)
	    {
		String folderName = Constants.MASTERSERVER_METAFILES_DIR
			+ names[i] + "/";
		File folder = new File(folderName);
		File[] listOfFiles = folder.listFiles();

		ArrayList<Long> fileNums = new ArrayList<Long>();
		for (File file : listOfFiles)
		{
		    try
		    {
			fileNums.add(Long.parseLong(file.getName()));
		    } catch (Exception e)
		    {
			e.printStackTrace();
		    }
		}

		// sort in ascending order..
		java.util.Collections.sort(fileNums);
		if (fileNums.size() > 50)
		{
		    Path pathToDelete = Paths.get(folderName + fileNums.get(0));
		    try
		    {
			Files.delete(pathToDelete);
		    } catch (IOException e)
		    {
			e.printStackTrace();
		    }

		}

		if (names[i].equals("logs"))
		    continue;

		String pathname = Constants.MASTERSERVER_METAFILES_DIR
			+ names[i] + "/" + mills;
		Path path = Paths.get(pathname);

		try
		{
		    Files.createFile(path);
		} catch (Exception e)
		{
		    System.err.println("Backup file already exists!");
		    return;
		}

		BufferedOutputStream outputStream = null;
		ObjectOutputStream out = null;

		try
		{
		    outputStream = new BufferedOutputStream(
			    new FileOutputStream(pathname));
		    out = new ObjectOutputStream(outputStream);
		    if (i == 0)
		    {
			filesSemaphore.acquire();
			out.writeObject(files);
		    }
		    if (i == 1)
		    {
			usersSemaphore.acquire();
			out.writeObject(users);
		    }
		    if (i == 2)
		    {
			dataServersSemaphore.acquire();
			out.writeObject(dataServers);
		    }

		} catch (Exception e)
		{
		    System.err.println("Backup on master server failed on - "
			    + i);
		} finally
		{
		    try
		    {
			out.flush();
			out.close();
			outputStream.close();
			remoteBackUp(pathname);
		    } catch (Exception e)
		    {
			e.printStackTrace();
		    }

		    if (i == 0)
			filesSemaphore.release();
		    if (i == 1)
			usersSemaphore.release();
		    if (i == 2)
			dataServersSemaphore.release();
		}
	    }

	    logManager.changeLogFile(mills);
	    System.out.println("Completed one backup to local disk.");

	}

	public void remoteBackUp(String filename) throws Exception
	{
	    Path pathname = Paths.get(filename);
	    if (!Files.exists(pathname))
	    {
		System.out.println("File for remote backup does not exist!");
		return;
	    }

	    RemoteSaverInterface remoteSaver1 = null, remoteSaver2 = null;
	    Registry registry = null;
	    String hostname1, hostname2;
	    if (myHostname.equals(Constants.MASTER_SERVER_1_IP))
	    {
		hostname1 = Constants.MASTER_SERVER_2_IP;
		hostname2 = Constants.MASTER_SERVER_3_IP;
	    } else if (myHostname.equals(Constants.MASTER_SERVER_2_IP))
	    {
		hostname1 = Constants.MASTER_SERVER_1_IP;
		hostname2 = Constants.MASTER_SERVER_3_IP;
	    } else
	    {
		hostname1 = Constants.MASTER_SERVER_1_IP;
		hostname2 = Constants.MASTER_SERVER_2_IP;
	    }

	    try
	    {
		if (InetAddress.getByName(hostname1).isReachable(1000))
		{
		    registry = LocateRegistry.getRegistry(hostname1);
		    remoteSaver1 = (RemoteSaverInterface) registry
			    .lookup(Constants.REMOTE_SAVER_NAME);

		}
	    } catch (Exception e)
	    {
		System.out.println("Failed to connect to RemoteSaver 1!");
	    }

	    try
	    {
		if (InetAddress.getByName(hostname2).isReachable(1000))
		{
		    registry = LocateRegistry.getRegistry(hostname2);
		    remoteSaver2 = (RemoteSaverInterface) registry
			    .lookup(Constants.REMOTE_SAVER_NAME);

		}
	    } catch (Exception e)
	    {
		System.out.println("Failed to connect to RemoteSaver 2!");
	    }

	    for (int j = 0; j < 2; j++)
	    {
		SeekableByteChannel channel = null;
		byte[] bytes = new byte[Constants.DATA_CHUNK_SIZE];
		int offset = 0;

		boolean isLast = false;
		while (isLast == false)
		{
		    if (j == 0 && remoteSaver1 == null)
		    {
			break;
		    }
		    if (j == 1 && remoteSaver2 == null)
		    {
			break;
		    }
		    try
		    {
			channel = Files.newByteChannel(pathname);
			channel.position(offset);

			ByteBuffer readBuffer = ByteBuffer.wrap(bytes);
			readBuffer.clear();

			int length = channel.read(readBuffer);
			if (length < 0)
			    length = 0;
			byte[] newBytes = Arrays.copyOf(bytes, length);

			DataChunk dataChunk = new DataChunk(newBytes);
			dataChunk.setOffset(offset);
			readBuffer = ByteBuffer.allocate(1);
			if (channel.read(readBuffer) <= 0)
			{
			    dataChunk.setLast(true);
			    isLast = true;
			} else
			{
			    dataChunk.setLast(false);
			    isLast = false;
			}
			offset = length;
			try
			{
			    if (j == 0)
				remoteSaver1
					.remoteSaveFile(dataChunk, filename);
			    else if (j == 1)
				remoteSaver2
					.remoteSaveFile(dataChunk, filename);
			} catch (CloudException e)
			{
			    System.out.println("Remote backup failed");
			    e.printStackTrace();
			    isLast = true;
			}

		    } catch (Exception e)
		    {
			e.printStackTrace();
			isLast = true;
		    } finally
		    {
			try
			{
			    channel.close();
			} catch (IOException e)
			{
			    e.printStackTrace();
			}
		    }
		}
	    }

	}
    }

    class BackUpLogThread implements Runnable
    {

	@Override
	public void run()
	{
	    while (true)
	    {
		if (isLogChanged)
		{
		    try
		    {
			new MasterServer.Housekeeper().remoteBackUp(logManager
				.getLatestLogFileName());
		    } catch (Exception e)
		    {
			// do nothing
		    }
		    isLogChanged = false;
		}
		try
		{
		    Thread.sleep(Constants.LOG_MONITOR_SLEEP_TIME);
		} catch (InterruptedException e)
		{
		    e.printStackTrace();
		}
	    }

	}

    }

    public static void main(String[] args)
    {

	if (args.length == 0)
	{
	    System.out
		    .println("usage: java masterserver.MasterServer serverNo.");
	    System.exit(-1);
	}

	int serverNo = 0;
	try
	{
	    serverNo = Integer.parseInt(args[0]);
	} catch (Exception e)
	{
	    System.out.println("serverNo. must be an integer!");
	    System.exit(-1);
	}

	String myHostname = null;
	switch (serverNo)
	{
	case 1:
	    myHostname = Constants.MASTER_SERVER_1_IP;
	    break;
	case 2:
	    myHostname = Constants.MASTER_SERVER_2_IP;
	    break;
	case 3:
	    myHostname = Constants.MASTER_SERVER_3_IP;
	    break;
	default:
	    myHostname = Constants.MASTER_SERVER_1_IP;
	    break;
	}

	System.setProperty("java.rmi.server.hostname", myHostname);

	MasterServerInterface myInterface = null;
	try
	{
	    myInterface = new MasterServer(myHostname);
	} catch (Exception e)
	{
	    e.printStackTrace();
	    System.exit(1);
	}

	Registry registry = null;

	try
	{
	    // create the registry and bind the name and object.
	    try
	    {
		registry = LocateRegistry
			.createRegistry(Constants.REGISTRY_PORT);
	    } catch (Exception failToCreateException)
	    {
		registry = LocateRegistry.getRegistry(Constants.REGISTRY_PORT);
	    }
	    registry.rebind(Constants.MASTER_SERVER_NAME, myInterface);
	} catch (Exception e)
	{
	    System.out.println(e.getMessage());
	    System.out.println("System exit!");
	    System.exit(-1);
	}

	System.out.println("Master Server initialized Successfully!!");

    }

    @Override
    public boolean testConnection() throws RemoteException
    {
	return true;
    }

}
